package robot.calibeta;

import lejos.nxt.Sound;
import chair.Chronos;
import chair.Queue;

/**
 * Une classe qui va gérer le haut-parleur du robot au sein d'un Thread séparé.
 * 
 * Utilisé pour jouer des sons ou des mélodies : donner quelques indications sur
 * le robot (attend balise RFID, a été puni ou récompensé, etc.).
 * 
 * Permet aussi de simuler le rythme cardiaque. Désactivé par défaut, utiliser
 * enableRythm() pour lancer les battements.
 * 
 * @author nomhad
 * 
 */
public class Voice extends Thread {

	/** L'instance du thread */
	private static Voice _singleton = new Voice();

	private static boolean _hasRythm = false;
	/** Nombre de ms par défaut entre deux pulsations */
	private static final long DEFAULT_RYTHM = 1000;
	/** Temps minimum entre deux pulsations */
	private static final long RYTHM_MIN = 100;
	/** Temps maximum entre deux pulsations */
	private static final long RYTHM_MAX = 2000;
	/** Temps actuel entre deux pulsations */
	private static long _rythmPollingTime = DEFAULT_RYTHM;

	/**
	 * De combien modifier la latence entre deux pulsations suivant
	 * récompense/punition
	 */
	private static final long RYTHM_STEP = 300;
	/** Quand a eu lieu la dernière pulsation */
	// private long _Top = -1;
	/** Quand doit avoir lieu la prochaine pulsation */
	private long _nextTop = -1;

	/** File des notes à jouer. */
	private Queue _notesQueue = new Queue();
	/** Marque si le Thread est lancé, évite synchronization à chaque get() */
	private static boolean _init = false;

	/**
	 * Établit des correspondances entre les notes et les fréquences, permet
	 * aussi de les jouer (avec cette utilisation c'est le Thread appelant qui
	 * joue son)
	 * 
	 * par exemple pour fréquences :
	 * http://jeanjacques.dialo.free.fr/frequenc.htm
	 * 
	 * En interne : positif pour fréquence, négatif pour durée de pause
	 * 
	 */
	public static enum Note {
		DO(262), DO_D(277), RE(294), RE_D(311), MI(330), FA(349), FA_D(370), SOL(
				392), SOL_D(415), LA(440), LA_D(466), SI(494),
		/** Passe à un octave supérieur (max : 5) */
		OCT_SUP,
		/** Passe à un octave inférieur (min : 0) */
		OCT_INF,
		/** Double durée des notes */
		DOUBLE,
		/** Divise par deux durée des notes (min : 1ms) */
		HALF,
		/** Rien ne sera joué */
		SILENCE,
		/** Marque la fin d'une mélodie, réinitialise octave et durée au besoin */
		RESET;
		/** Fréquence correspondante à la note */
		private int frequency = -1;
		/** Pendant combien de temps jouer une note par défaut via play() */
		private static final int DEFAULT_DURATION_TIME = 200;
		/** Durée actuelle des notes */
		private static int _durationTime = DEFAULT_DURATION_TIME;
		/** Octave par défaut */
		private static final int DEFAULT_OCTAVE = 3;
		/** Octave actuel */
		private static int _octave = DEFAULT_OCTAVE;
		/**
		 * Un intervalle de silence supplémentaire qui permet de séparer les
		 * notes
		 */
		private static int BETWEEN_NOTES = 10;

		Note() {
		}

		Note(int freq) {
			frequency = freq;
		}

		/**
		 * Joue une note pendant un temps fixé
		 */
		void play() {
			play(_durationTime);
		}

		/**
		 * Joue cette note pendant un temps donné (met le Thread en pause)
		 * 
		 * @param duration
		 *            pendant combien de temps la note doit être jouée
		 */
		void play(int duration) {
			// Si c'est bien une note : on joue
			if (frequency > 0) {
				// Fréquence à moduler suivant l'octave
				int freq = frequency;
				// De combien d'octaves de différence ?
				int diffOctave = _octave - DEFAULT_OCTAVE;
				if (diffOctave != 0) {
					// Si plus bas : divise fréquence de 2^diffOctave
					if (diffOctave < 0)
						freq /= 1 << (-diffOctave);
					// Sinon multiplie par 2^diffOctave
					else
						freq *= 1 << diffOctave;
				}
				Sound.playTone(freq, duration);
				// Attend que la note joue
				try {
					sleep(duration + BETWEEN_NOTES);
				} catch (InterruptedException e) {
				}
				// Le reste nous concerne plus
				return;
			}
			// S'occupe de tout ce qui n'est pas note
			switch (this) {
			case OCT_INF:
				if (_octave > 0)
					_octave--;
				break;
			case OCT_SUP:
				if (_octave < 6)
					_octave++;
				break;
			case DOUBLE:
				_durationTime *= 2;
				break;
			case HALF:
				if (_durationTime > 2)
					_durationTime /= 2;
				break;
			case RESET:
				_octave = DEFAULT_OCTAVE;
				_durationTime = DEFAULT_DURATION_TIME;
				break;
			case SILENCE:
				// Met en pause le Thread le temps de la pause
				try {
					sleep(duration + BETWEEN_NOTES);
				} catch (InterruptedException e) {
				}
				break;
			}
		}
	}

	/**
	 * Des suites de notes toutes prêtes à jouer suivant l'occasion. Demander à
	 * une mélodie de jouer revient la mettre dans la file de Voice la suite des
	 * notes qui la composent.
	 * 
	 * @author nomhad
	 * 
	 */
	public static enum Melody {
		/**
		 * Le robot s'allume, on débute bien la journée (cf
		 * http://www.flutetunes.com/tunes.php?id=120)
		 */
		ALSO_SPRACH_ZARATHUSTRA(Note.DOUBLE, Note.DO, Note.SOL, Note.OCT_SUP,
				Note.DOUBLE, Note.DO, Note.RESET),
		/**
		 * Le robot se rappelle à notre bon souvenir (utilisé typiquement pour
		 * signaler que capteur RFID prêt pour lecture)
		 */
		HELLO(Note.OCT_SUP, Note.HALF, Note.DO, Note.MI, Note.RESET),
		/** Le robot s'attend à quelque chose de bien */
		GOOD(Note.OCT_SUP, Note.DO, Note.FA, Note.RESET),
		/** Le robot s'attend à quelque chose de mal */
		BAD(Note.FA_D, Note.DO_D, Note.RESET),
		/**
		 * Mis du temps à trouver : motif du destin, cf
		 * http://fr.wikipedia.org/wiki/Symphonie_n%C2%B0_5_de_Beethoven
		 */
		BEETHOVEN(Note.OCT_INF, Note.SOL, Note.SOL, Note.SOL, Note.DOUBLE,
				Note.RE_D, Note.SILENCE, Note.HALF, Note.FA, Note.FA, Note.FA,
				Note.DOUBLE, Note.RE, Note.RESET),
		/** Version courte */
		BEETHOVEN_SHORT(Note.OCT_INF, Note.SOL, Note.SOL, Note.SOL,
				Note.DOUBLE, Note.RE_D, Note.RESET),
		/**
		 * Découverte d'un nouvel objet, cf:
		 * http://www.3djuegos.com/foros/tema/1400186/0/melodias-para-el-pueblo/
		 * 
		 */
		ZELDA_ITEM_FANFARE(Note.OCT_SUP, Note.LA, Note.SI, Note.OCT_SUP,
				Note.DO, Note.DOUBLE, Note.RE, Note.RESET);

		/** La suite de note qui compose cette mélodie */
		private Note[] melody;

		/**
		 * Quelle est donc cette mélodie sur le bout de la langue ?
		 * 
		 * @param notes
		 *            les notes qui composent la mélodie
		 */
		Melody(Note... notes) {
			melody = notes;
		}

		/**
		 * Joue la mélodie (place ses notes dans file de Voice)
		 */
		public void play() {
			// Plus lisible : la file de voice
			Queue file = Voice.get()._notesQueue;
			synchronized (file) {
				// Parcourt les notes de la mélodie et les place dans la file
				for (int i = 0; i < melody.length; i++)
					file.push(melody[i]);
			}
			// Réveil Voice pour qu'il noue joue ça au plus vite
			Voice.get().wakeUp();
		}
	}

	/**
	 * Lance le Thread, utilisé par get() si drapeau _init == false
	 */
	private static void init() {
		synchronized (_singleton) {
			if (!_singleton.isAlive()) {
				_init = true;
				_singleton.setDaemon(true);
				_singleton._nextTop = Chronos.top() + _rythmPollingTime;
				_singleton.setPriority(Thread.MAX_PRIORITY);
				_singleton.start();
			}
		}
	}

	/**
	 * Retourne l'instance du thread
	 * 
	 * FIXME: bug latent lors d'initialisation, parfois tente start() deux fois
	 * (??)
	 * 
	 * @return Instanciation de Voice
	 */
	private static Voice get() {
		if (!_init)
			init();
		return _singleton;
	}

	/**
	 * Produit le son d'un battement cardiaque
	 */
	private void heartPulse() {
		// Note.SI.play(150);
		Note.SOL.play(50);
	}

	/**
	 * Change le rythme cardiaque suivant la sanction reçue par le robot. Borné
	 * par RYTHM_MIN et RYTHM_MAX
	 * 
	 * @param reward
	 *            si positif : récompense (diminue rythme), si négatif punition
	 *            (augmente rythme)
	 */
	public synchronized static void alterRythm(int reward) {
		long rythm = _rythmPollingTime + reward * RYTHM_STEP;
		if (rythm < RYTHM_MIN)
			rythm = RYTHM_MIN;
		else if (rythm > RYTHM_MAX)
			rythm = RYTHM_MAX;
		_rythmPollingTime = rythm;
	}

	/**
	 * Fixe le rythme cardiaque.
	 * 
	 * @param reward
	 *            0 pour rythme par défaut, vers les négatif accèlère, vers les
	 *            positif augmente
	 */
	public synchronized static void setRythm(int reward) {
		_rythmPollingTime = DEFAULT_RYTHM;
		alterRythm(reward);
	}

	/**
	 * Vérifie s'il est temps de produire pulsation, si c'est le cas en profite
	 * pour incrémenter le moment où aura lieu la prochaine
	 * 
	 * @return true si doit faire jouer pulsation, false sinon
	 */
	private boolean timesUp() {
		long top = Chronos.top();
		// Il est l'heure, incrémente avant de renvoyer true
		if (top >= _nextTop) {
			_nextTop = top + _rythmPollingTime;
			return true;
		}
		return false;
	}

	/**
	 * Endort le Thread jusqu'à-ce qu'il soit temps de faire jouer pulsation (si
	 * _hasRythm) ou bien pour une durée indéterminée
	 */
	private void waitUntilPulse() {
		// On va attendre jusqu'à-ce que des notes jouent
		long waitTime = 0;
		// Ou bien jusqu'à pulsation si activé
		if (_hasRythm)
			waitTime = _nextTop - Chronos.top();

		if (waitTime >= 0) {
			synchronized (_singleton) {
				try {
					_singleton.wait(waitTime);
				} catch (InterruptedException e) {
				}
			}
		}
	}

	@Override
	public void run() {
		// FIXME: Il lui faut un premier temps mort, sinon va manger une note ??
		Note.SILENCE.play();
		for (;;) {
			// Si on a été réveillé par l'extérieur, il y a des notes à jouer
			playQueue();
			// Si fonctionnalité activé, il est temps d'émettre une pulsation
			if (_hasRythm && timesUp()) {
				heartPulse();
			}
			// Pause jusqu'à prochaine pulsation (ou jusqu'à nouvelles notes si
			// rythme désactivé)
			waitUntilPulse();
		}
	}

	/**
	 * Va jouer toutes les notes de la file
	 */
	private void playQueue() {
		synchronized (_notesQueue) {
			while (!_notesQueue.isEmpty())
				((Note) _notesQueue.pop()).play();
		}
	}

	/**
	 * Réveil le Thread, utilisé pour jouer les notes dans la file sans attendre
	 * prochain battement
	 */
	private void wakeUp() {
		synchronized (_singleton) {
			_singleton.interrupt();
		}
	}

	/**
	 * Active ou désactive les pulsations cardiques
	 * 
	 * @param toggle
	 *            true pour activer le rythme cardique, false sinon
	 */
	public static void enableRythm(boolean toggle) {
		_hasRythm = toggle;
	}

}
